/*******************************************************************************
 * Copyright (c) 2000, 2006 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.test.internal.performance.eval;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.test.internal.performance.InternalPerformanceMeter;
import org.eclipse.test.internal.performance.data.DataPoint;
import org.eclipse.test.internal.performance.data.Dim;
import org.eclipse.test.internal.performance.data.Scalar;
import org.eclipse.test.internal.performance.eval.StatisticsUtil.Percentile;
import org.junit.Assert;

/**
 * @since 3.1
 */
public class StatisticsSession {

    static final class Statistics {

        public long   count;
        public long   sum;
        public double average;
        public double stddev;
    }

    private final DataPoint[] fDataPoints;
    private final Map<Dim, Statistics>         fStatistics = new HashMap<>();

    public StatisticsSession(DataPoint[] datapoints) {
        fDataPoints = datapoints;
    }

    public double getAverage(Dim dimension) {
        return getStats(dimension).average;
    }

    public long getSum(Dim dimension) {
        return getStats(dimension).sum;
    }

    public long getCount(Dim dimension) {
        return getStats(dimension).count;
    }

    public double getStddev(Dim dimension) {
        return getStats(dimension).stddev;
    }

    double getStderr_mean(Dim dimension) {
        return getStats(dimension).stddev / Math.sqrt(getStats(dimension).count);
    }

    double getStudentsT(Dim dimension, Percentile percentile) {
        int df = (int) getStats(dimension).count - 1;
        return StatisticsUtil.getStudentsT(df, percentile);
    }

    /**
     * Returns the confidence interval for the given dimension and the percentile.
     *
     * @param dimension
     *            the dimension
     * @param percentile
     *            the percentile
     * @return an array of length two, with the lower and upper bounds of the confidence interval
     */
    public double[] getConfidenceInterval(Dim dimension, Percentile percentile) {
        double mean = getAverage(dimension);
        double stat_err = getStderr_mean(dimension);

        double t = getStudentsT(dimension, percentile);

        double[] interval = { mean - t * stat_err, mean + t * stat_err };

        return interval;
    }

    private Statistics getStats(Dim dimension) {
        Statistics stats = fStatistics.get(dimension);
        if (stats == null) {
            stats = computeStats(dimension);
            fStatistics.put(dimension, stats);
        }
        return stats;
    }

    private Statistics computeStats(Dim dimension) {

        Statistics stats;

        Set<Integer> steps = new HashSet<>();
        for (DataPoint dp : fDataPoints) {
            steps.add(Integer.valueOf(dp.getStep()));
        }

        if (steps.contains(Integer.valueOf(InternalPerformanceMeter.AVERAGE))) {
            // an already aggregated set of data points from the DB
            stats = computeStatsFromAggregates(dimension);
        } else if (steps.contains(Integer.valueOf(InternalPerformanceMeter.AFTER))) {
            // raw values from measurement
            stats = computeStatsFromMeasurements(dimension, steps);
        } else {
            Assert.fail("illegal data set: contains neither AVERAGE nor AFTER values."); //$NON-NLS-1$
            stats = null; // dummy
        }

        return stats;
    }

    private Statistics computeStatsFromAggregates(Dim dimension) {
        Statistics stats = new Statistics();
        long aggregateCount = 0;
        double averageSum = 0;
        long countSum = 0;
        double stdevSum = 0;

        // Set acquiredAggregates= new HashSet();
        for (DataPoint point : fDataPoints) {
            Scalar scalar = point.getScalar(dimension);
            if (scalar == null)
                continue;

            Integer aggregate = Integer.valueOf(point.getStep());
            // allow for multiple measurements that were each stored with their own
            // aggregate values
            // Assert.assertTrue(acquiredAggregates.add(aggregate));

            long magnitude = scalar.getMagnitude();
            switch (aggregate.intValue()) {
                case InternalPerformanceMeter.AVERAGE:
                    averageSum += magnitude;
                    aggregateCount++;
                    break;
                case InternalPerformanceMeter.STDEV:
                    // see DB.internalStore
                    stdevSum += Double.longBitsToDouble(magnitude);
                    break;
                case InternalPerformanceMeter.SIZE:
                    countSum += magnitude;
                    break;
                default:
                    Assert.fail("only average, stdev and size are supported in aggregate mode"); //$NON-NLS-1$
                    break;
            }
        }

        stats.average = averageSum / aggregateCount;
        stats.stddev = stdevSum / aggregateCount; // XXX this does not work! have to treat multiple runs like normal measurement
                                                  // data
        stats.count = countSum;
        stats.sum = Math.round(stats.count * stats.average);

        return stats;
    }

    private Statistics computeStatsFromMeasurements(Dim dimension, Set<Integer> steps) {
        Statistics stats = new Statistics();
        long mags[];
        switch (steps.size()) {
            case 1:
                // if there is only one Step, we don't calculate the delta. happens for startup tests
                mags = new long[fDataPoints.length];
                for (int i = 0; i < fDataPoints.length; i++) {
                    Scalar sc = fDataPoints[i].getScalar(dimension);
                    mags[i] = sc == null ? 0 : sc.getMagnitude();
                }
                break;
            case 2:
                int count = fDataPoints.length / 2;
                mags = new long[count];
                for (int i = 0; i < count; i++) {
                    DataPoint before = fDataPoints[2 * i];
                    Assert.assertTrue("wrong order of steps", before.getStep() == InternalPerformanceMeter.BEFORE); //$NON-NLS-1$
                    DataPoint after = fDataPoints[2 * i + 1];
                    Assert.assertTrue("wrong order of steps", after.getStep() == InternalPerformanceMeter.AFTER); //$NON-NLS-1$

                    Scalar delta = getDelta(before, after, dimension);
                    long magnitude = delta.getMagnitude();
                    mags[i] = magnitude;
                }
                break;
            default:
                Assert.fail("cannot handle more than two steps in measurement mode"); //$NON-NLS-1$
                return null; // dummy
        }

        for (long mag : mags) {
            stats.sum += mag;
            stats.count++;
        }

        if (stats.count > 0) {
            stats.average = (double) stats.sum / stats.count;
            if (stats.count == 1) {
                stats.stddev = 0;
            } else {
                double squaredDeviations = 0;
                for (long mag : mags) {
                    double deviation = stats.average - mag;
                    squaredDeviations += deviation * deviation;
                }
                stats.stddev = Math.sqrt(squaredDeviations / (stats.count - 1)); // unbiased sample stdev
            }
        } else {
            stats.average = 0;
            stats.stddev = 0;
        }

        return stats;
    }

    private Scalar getDelta(DataPoint before, DataPoint after, Dim dimension) {
        Scalar one = before.getScalar(dimension);
        Assert.assertTrue("reference has no value for dimension " + dimension, one != null); //$NON-NLS-1$

        Scalar two = after.getScalar(dimension);
        Assert.assertTrue("reference has no value for dimension " + dimension, two != null); //$NON-NLS-1$

        return new Scalar(one.getDimension(), two.getMagnitude() - one.getMagnitude());
    }

    public boolean contains(Dim dimension) {
        if (fDataPoints.length > 0)
            return fDataPoints[0].contains(dimension);
        return false;
    }
}
